package org.heydr.common.http;

import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.ibm.icu.text.CharsetDetector;
import com.ibm.icu.text.CharsetMatch;

/**
 * 
 * 
 */
public class EncodingDetector {
	private static final Logger log = LoggerFactory
			.getLogger(EncodingDetector.class);
	public static final int NO_THRESHOLD = -1;

	private static final HashMap<String, String> ALIASES = new HashMap<String, String>();

	private static final HashSet<String> DETECTABLES = new HashSet<String>();

	// CharsetDetector will die without a minimum amount of data.
	private static final int MIN_LENGTH = 4;

	// <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
	private static final Pattern metaCharsetPt = Pattern
			.compile("(?i)<meta (.*?)charset=(.*?)[\"']");
	// <?xml version="1.0" encoding="gb2312"?>
	private static final Pattern xmlEncodingPt = Pattern
			.compile("(?i)<?xml (.*?)encoding=[\"'](.*?)[\"']");
	// text/html; charset=utf-8
	private static final Pattern contentTypePt = Pattern
			.compile("(?i)text/html.*charset=(.*)");

	static {
		DETECTABLES.add("text/html");
		DETECTABLES.add("text/plain");
		DETECTABLES.add("text/richtext");
		DETECTABLES.add("text/rtf");
		DETECTABLES.add("text/sgml");
		DETECTABLES.add("text/tab-separated-values");
		DETECTABLES.add("text/xml");
		DETECTABLES.add("application/rss+xml");
		DETECTABLES.add("application/xhtml+xml");
		/*
		 * the following map is not an alias mapping table, but maps character
		 * encodings which are often used in mislabelled documents to their
		 * correct encodings. For instance, there are a lot of documents
		 * labelled 'ISO-8859-1' which contain characters not covered by
		 * ISO-8859-1 but covered by windows-1252. Because windows-1252 is a
		 * superset of ISO-8859-1 (sharing code points for the common part),
		 * it's better to treat ISO-8859-1 as synonymous with windows-1252 than
		 * to reject, as invalid, documents labelled as ISO-8859-1 that have
		 * characters outside ISO-8859-1.
		 */
		ALIASES.put("ISO-8859-1", "windows-1252");
		ALIASES.put("EUC-KR", "x-windows-949");
		ALIASES.put("x-EUC-CN", "GB18030");
		ALIASES.put("GBK", "GB18030");
		// ALIASES.put("Big5", "Big5HKSCS");
		// ALIASES.put("TIS620", "Cp874");
		// ALIASES.put("ISO-8859-11", "Cp874");

	}

	private List<EncodingClue> clues;
	private int minConfidence;

	private CharsetDetector detector;

	public EncodingDetector(int minConfidence) {
		detector = new CharsetDetector();
		clues = new ArrayList<EncodingClue>();
		this.minConfidence = minConfidence;
	}

	public EncodingDetector() {
		this(50);
	}

	private void addClue(String value, String source, int confidence) {
		if (value == null || "".equals(value)) {
			return;
		}
		value = resolveEncodingAlias(value);
		if (value != null) {
			clues.add(new EncodingClue(value, source, confidence));
		}
	}

	private void clearClue() {
		clues.clear();
	}

	public synchronized Set<String> guessEncoding(byte[] data, boolean filter) {
		clearClue();
		if (minConfidence >= 0 && data.length > MIN_LENGTH) {
			CharsetMatch[] matches = null;

			// do all these in a try/catch; setText and detect/detectAll
			// will sometimes throw exceptions
			try {
				detector.enableInputFilter(filter);
				if (data.length > MIN_LENGTH) {
					detector.setText(data);
					matches = detector.detectAll();
				}
			} catch (Exception e) {
				log.debug("Exception from ICU4J (ignoring): ");
			}

			if (matches != null) {
				for (CharsetMatch match : matches) {
					addClue(match.getName(), "detect", match.getConfidence());
					// System.out.println(match.getName() + " detect "
					// + match.getConfidence());
				}
			}
		}

		Set<String> bestCharSets = new HashSet<String>();
		if (clues.size() > 0) {
			Collections.sort(clues);
			EncodingClue bestClue = Collections.min(clues);
			bestCharSets.add(bestClue.value);
			for (EncodingClue clue : clues) {
				if (minConfidence >= 0
						&& clue.confidence == bestClue.confidence
						&& clue.confidence >= minConfidence) {
					bestCharSets.add(clue.value);
				} else {
					break;
				}
			}
		}

		return bestCharSets;
	}

	public static String resolveEncodingAlias(String encoding) {
		if (encoding == null || !Charset.isSupported(encoding))
			return null;
		String canonicalName = new String(Charset.forName(encoding).name());
		return ALIASES.containsKey(canonicalName) ? ALIASES.get(canonicalName)
				: canonicalName;
	}

	public static String parseCharsetFromContentType(String contentType) {
		String encode = null;
		if (contentType != null) {
			Matcher mc = contentTypePt.matcher(contentType);
			if (mc.find()) {
				encode = mc.group(1).trim();
			}
		}
		return encode;

	}

	public static String parseCharsetFromContent(byte[] content) {
		String encode = null;
		if (content != null) {
			String htmlContent = new String(content);
			Matcher mc = metaCharsetPt.matcher(htmlContent);
			if (mc.find()) {
				encode = mc.group(2).trim();
			} else { // 否则是<?xml version="1.0" encoding="gb2312"?>格式的
				mc = xmlEncodingPt.matcher(htmlContent);
				if (mc.find()) {
					encode = mc.group(2).trim();
				}
			}
		}
		return encode;

	}

	public String getEncoding(String contentType, byte[] content) {
		String encode = null;
		try {
			// encode = parseCharsetFromContent(content);
			// // log.debug("encode0 : " + encode);
			//
			// if (encode == null) {
			encode = parseCharsetFromContentType(contentType);
			// log.debug("encode2 : " + encode);
			// }
			if (encode == null) {
				Set<String> encodings = guessEncoding(content, false);
				for (String charset : encodings) {
					if (charset.toLowerCase().indexOf("utf") != -1) {
						encode = charset;
						break;
					} else if (charset.toLowerCase().indexOf("gb") != -1) {
						encode = charset;
					}
				}
				// log.debug("encode3 : " + encode);
			}
			if (encode != null) {
				if (encode.toLowerCase().indexOf("utf") == -1) {
					encode = "GB18030";
				} else {
					encode = "UTF-8";
				}
			} else {
				encode = "GB18030";
			}

			// log.debug("encode4 : " + encode);
		} catch (Exception e) {
			log.error(e.getMessage(), e);
			// encode = "GB18030";
		}

		return encode;
	}

	private class EncodingClue implements Comparable<EncodingClue> {
		private String value;
		private String source;
		private int confidence;

		public EncodingClue(String value, String source, int confidence) {
			this.value = value.toLowerCase();
			this.source = source;
			this.confidence = confidence;
		}

		public String toString() {
			return value
					+ " ("
					+ source
					+ ((confidence >= 0) ? ", " + confidence + "% confidence"
							: "") + ")";
		}

		public int compareTo(EncodingClue o) {

			return o.confidence - this.confidence;
		}

	}

}
